/*
 * Module.java
 *
 * Created on May 17, 2007, 6:13 PM
 *
 *  Copyright (C) 2010 Tal Eisenberg
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package tal.drivechain.plugin;

import java.lang.reflect.InvocationTargetException;
import tal.drivechain.system.PluginLoader;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import tal.drivechain.chain.AbortChainException;
import tal.drivechain.chain.ChainNode;
import tal.drivechain.chain.NodeListener;
import tal.drivechain.chain.shell.InfoPrinter;
import tal.drivechain.module.properties.PropertyString;
import tal.drivechain.variable.types.DataType;
import tal.drivechain.variable.Variable;
import tal.drivechain.module.properties.PropertiesManager;
import tal.drivechain.module.xmlconfig.PropertyInfo;
import tal.drivechain.parsing.Tokenizer;
import tal.drivechain.variable.Command;
import tal.drivechain.variable.SetProperty;
import tal.drivechain.variable.VarTableAction;

/**
 * This class provides the framework for every Module class and provides Id and logging support as
 * well as some basic functionality.
 *
 * @author Tal Eisenberg
 */
public abstract class Module extends ChainNode {

    protected ModulePackageManager packageManager = null;
    protected PropertiesManager props = null;
    private String fullClassName;
    private String className;

    protected Variable inRef = new Variable("input", DataType.VAR, null, true);
    protected Variable inTimestamp = new Variable("timestamp", DataType.LONG, null, true);
    protected Variable inChannel = new Variable("channel", DataType.INT, null, true);
    private VarTableAction refAction = new VarTableAction("input", VarTableAction.Action.SET_ANY);
    private VarTableAction timestampAction = new VarTableAction("timestamp", VarTableAction.Action.SET_ANY);
    private VarTableAction channelAction = new VarTableAction("channel", VarTableAction.Action.SET_ANY);

    private Method[] modCommands;
    private String INPUT = "input";

    /**
     * Constructor for the Module class.
     * 
     */
    public Module() {
        fullClassName = this.getClass().getName();
        className = this.getClass().getSimpleName();
        ModulePackageManager p = PluginLoader.getDefaultPackage();
        if (p!=null && fullClassName.startsWith(p.getPackageConfiguration().getPackageInfo().getName().toLowerCase() + ".")) {
            fullClassName = className;
        }

        // Collect all command methods
        List<Method> methods = new ArrayList<Method>();
        for (Method m : getClass().getMethods()) {
            if (!m.getDeclaringClass().isAssignableFrom(Module.class) && m.getReturnType()==void.class && !m.getName().startsWith("set")) {
                boolean pass = true;
                for (Class c : m.getParameterTypes()) {
                    if (DataType.findType(c)==null) {
                        pass = false;
                    }
                }

                if (pass) {
                    methods.add(m);
                }
            }
        }
        modCommands = methods.toArray(new Method[0]);
    }

    /**
     * Returns the name of the module class
     */
    public final String getNodeClassName() { return fullClassName; }

    /**
     * Returns the name of the module. w/o the package name.
     */
    public String getClassName() { return className; }

    public void setPackageManager(ModulePackageManager packageManager) throws IllegalArgumentException {
        this.packageManager = packageManager;
        this.nodeInfo = packageManager.getPackageConfiguration().getModulesInfo().get(getClassName());
        if (nodeInfo==null)
            throw new IllegalArgumentException("Can't find plugin " + getClassName() + " in package " + packageManager.getPackageConfiguration().getPackageInfo().getName());
        props = packageManager.getPackageConfiguration().getModulesInfo().get(className).createPropertiesManager();
    }
    
    /**
     * Called with the relevant command properties for this module.
     * Parsing of the properties and module setup steps happen here.
     * 
     * 
     * @param properties A {@link PropertyString} object containing properties for the module.
     *                  The parse() method has to be called before the object can be used.
     */
    public void setupProperties() throws IllegalArgumentException, AbortChainException {
        for (PropertyInfo info : nodeInfo.getProperties()) {
            String name = info.getName();
            this.setProperty(name, props.getProperty(name), null);
        }
    }
    
    public void parseProperties(String strProps) throws AbortChainException {
        if (props!=null) {
            (new PropertyString(strProps)).sendToManager(props);
        } else {
            if (!loggers.isEmpty())
                log("Program error. Trying to parse module properties before running properties manager.", MSG.ERROR);
            else {
                throw new AbortChainException("Program error. Trying to parse module properties before running properties manager.");
            }
        }
    }

    /**
     * Sends a {@link Variable} to the modules.
     *
     * @param in    A Variable object that will be received by the module.
     * @param timestamp Timing in milliseconds of the message.
     */
    @Override
    public void send(Variable in, long timestamp, Channel channel) {
        super.send(in, timestamp, channel);

        if (in.getDataType().matches(DataType.COMMAND) || in.getDataType().matches(DataType.COMMAND, DataType.MATCH_ARRAY) && channel == Channel.CHAIN) {
            this.setInVars(in, timestamp, channel);            
            invokeCommand(in);
        } else if (in.getDataType().matches(DataType.PROPERTY) && channel == Channel.CHAIN) {
            this.setInVars(in, timestamp, channel);            
            SetProperty prop = (SetProperty)in.getValue(this);
            if (debugFlag)
                log("Received a prop: " + prop.toString(), MSG.DEBUG);
            prop.invokeSetProperty(this);
        } else if (in.getDataType().matches(DataType.PROPERTY, DataType.MATCH_ARRAY) && channel == Channel.CHAIN) {
            this.setInVars(in, timestamp, channel);                        
            List<Object> internal = (List<Object>)in.getInternalValue();
            if (debugFlag)
                log("Received a prop array: " + in.toString(this), MSG.DEBUG);
            for (Object val : internal) {
                ((SetProperty)val).invokeSetProperty(this);
            }
        } else {
            if (channel == Channel.CHAIN)
                log("Received variable thru chain: " + in.toString(this), MSG.DEBUG);
            this.setInVars(in, timestamp, channel);
            msgReceived(in, timestamp, channel);
        }
    }
    
    protected abstract void msgReceived(Variable in, long timestamp, Channel channel);
    
    protected void setInVars(Variable in, long timestamp, Channel channel) {
        inRef.setValue(in.getValue(this), false);
        localVars.put(INPUT, inRef);
        //inTimestamp.setValue(timestamp, true);
        //inChannel.setValue(channel.getId(), true);
        //refAction.set(inRef, localVars, this);
        //timestampAction.set(inTimestamp, localVars, this);
        //channelAction.set(inChannel, localVars, this);
    }
    
    protected void invokeCommand(Variable cmdVar) {
        if (cmdVar.getDataType().matches(DataType.COMMAND, DataType.MATCH_ARRAY)) {
            List<Command> commands = (List<Command>)cmdVar.getValue(this);            
            HashMap<Command, Method> matchedMethods = new HashMap<Command,Method>();
            if (debugFlag) {
                String names = "";
                for (Command c : commands) names += c.getName() + ", ";
                log("Received command array: " + names.substring(0, names.length()-2), MSG.DEBUG);
            }            
            for (Command c : commands) {
                boolean match = false;
                for (Method m : modCommands) {
                    if (m.getName().equals(c.getName()) && m.getParameterTypes().length==c.getParameters().size()) {
                        matchedMethods.put(c, m);
                        match = true;
                        break;
                    }                        
                }
                if (!match) {
                    if (debugFlag) log("Unknown command " + c.getName() + " with " + c.getParameters().size() + " parameters. Processing as chain message.", MSG.DEBUG);
                    this.setInVars(cmdVar, -1, Channel.CHAIN);
                    this.msgReceived(cmdVar, -1, Channel.CHAIN);
                    return;
                }
            }
            
            for (Command c : commands) {
                try {
                    invokeCommandMethod(matchedMethods.get(c), c.getParameters());
                } catch (InvocationTargetException ie) {
                    log("Error while calling command " + c.getName() + ": " + ie.getTargetException(), MSG.WARNING);
                    if (debugFlag) ie.getTargetException().printStackTrace();
                } catch (IllegalArgumentException ie) {
                    log("Error while calling command " + c.getName() + ": " + ie, MSG.WARNING);
                    if (debugFlag) ie.printStackTrace();
                } catch (Exception ex) {
                    log("Error while calling command " + c.getName() + ": " + ex, MSG.WARNING);
                    if (debugFlag) ex.printStackTrace();
                }                            
            }
        } else {
            Command cmd = (Command)cmdVar.getValue(this);
            if (debugFlag) log("Received command: " + cmd.toString(this), MSG.DEBUG);
            Method cmdMethod = null;
            for (Method m : modCommands) {
                if (m.getName().equals(cmd.getName()) && m.getParameterTypes().length==cmd.getParameters().size()) cmdMethod = m;
            }
            if (cmdMethod==null) {
                if (debugFlag) log("Unknown command: " + cmd.getName() + " with " + cmd.getParameters().size() + " parameters. Processing as chain message.", MSG.WARNING);
                this.setInVars(cmdVar, -1, Channel.CHAIN);
                this.msgReceived(cmdVar, -1, Channel.CHAIN);
                return;
            }
            
            try {
                invokeCommandMethod(cmdMethod, cmd.getParameters());
            } catch (InvocationTargetException ie) {
                log("Error while calling command " + cmd.getName() + ": " + ie.getTargetException(), MSG.WARNING);
                if (debugFlag) ie.getTargetException().printStackTrace();
            } catch (IllegalArgumentException ie) {
                log("Error while calling command " + cmd.getName() + ": " + ie, MSG.WARNING);
                if (debugFlag) ie.printStackTrace();
            } catch (Exception ex) {
                log("Error while calling command " + cmd.getName() + ": " + ex, MSG.WARNING);
                if (debugFlag) ex.printStackTrace();
            }
            
        }
    }

    public Method[] getModuleCommands() {
        return modCommands;
    }
    
    @Override
    public Object getProperty(String propertyName) {
        PropertyInfo prop = null;
        for (PropertyInfo p : nodeInfo.getProperties()) {
            if (p.getName().equals(propertyName)) {
                prop = p;
                break;
            }
        }
        if (prop==null)
            throw new IllegalArgumentException("Module " + this.getNodeClassName() + " doesn't have a '" + propertyName + "' property");
        else {
            String methodName = "get" + prop.getNameForMethod();

            Method[] methods = getClass().getMethods();
            Method getMethod = null;
            for (Method method : methods) {
                if (method.getName().equals(methodName)) {
                    getMethod = method;
                }
            }
            if (getMethod==null)
                throw new IllegalArgumentException("Can't find getter method for " + propertyName + " (" + methodName + ")");
            try {
                //if (infoFlag)
                //    log("Getting value of " + propertyName, Module.MSG.INFO);

                Method m = getClass().getMethod(methodName);
                return m.invoke(this);
            } catch (NoSuchMethodException ne) {
                throw new IllegalArgumentException("Can't get the value of property " + propertyName + " ("  + methodName + "). ");
            } catch (IllegalArgumentException ie) {
                ie.printStackTrace();
                throw new IllegalArgumentException("Error while getting property " + propertyName + ": " + ie.getMessage());
            } catch (InvocationTargetException ie) {
                ie.getTargetException().printStackTrace();
                throw new IllegalArgumentException("Error while getting property " + propertyName + " (invocation): " + ie.getTargetException().getMessage());
            } catch (Exception ex) {
                ex.printStackTrace();
                throw new IllegalArgumentException("Error while getting property " + propertyName + " : " + ex.getCause());
            }
        }
    }

    @Override
    public Object getOriginalPropValue(String name) {
        return this.props.getProperty(name);
    }

    public void setProperty(String propName, Object val, SetProperty.Flag flag) {
        PropertyInfo propInfo = getNodeInfo().findPropertyInfo(propName);
        //if (infoFlag) log("Setting " + propInfo.getName() + " to " + InfoPrinter.printProperty(this, propInfo, val), Module.MSG.INFO);
        String methodName = "set" + PropertyInfo.propNameToMethodName(propInfo.getName());
        try {
            Method set = null;
            boolean hasFlag = false;
            for (Method m : getClass().getMethods()) {
                if (m.getName().equals(methodName)) {
                    if (m.getParameterTypes().length==2 && m.getParameterTypes()[1]==SetProperty.Flag.class) {
                        hasFlag = true;
                        set = m;
                    } else if (m.getParameterTypes().length==1) {
                        hasFlag = false;
                        set = m;
                    }
                }
            }
            if (set==null) {
                log("Property " + propInfo.getName() + " can't be set.", MSG.WARNING);
            } else {
                try {
                    if (hasFlag) {
                        set.invoke(this, set.getParameterTypes()[0].cast(val), flag);
                    } else {
                        set.invoke(this, set.getParameterTypes()[0].cast(val));
                    }
                    for (NodeListener l : nodeListeners) l.propertyChanged(this, propInfo.getName(), val);
                } catch (ClassCastException e) {
                    e.printStackTrace();
                    log("Can't cast " + val.getClass().getSimpleName() + " to " + set.getParameterTypes()[0].getSimpleName(), MSG.WARNING);
                }
            }
        } catch (IllegalArgumentException ie) {
            log("Error while setting property " + propInfo.getName() + ": " + ie, MSG.WARNING);
            if (debugFlag) ie.printStackTrace();
        } catch (InvocationTargetException e) {
            e.getTargetException().printStackTrace();
            log ("Error while setting property " + propInfo.getName() + ": " + e.getTargetException(), MSG.WARNING);
            if (debugFlag) e.printStackTrace();
        } catch (Exception ex) {
            log("Error while setting property " + propInfo.getName() + ": " + ex, MSG.WARNING);
            if (debugFlag) ex.printStackTrace();
        }
    }

    public ModulePackageManager getPackageManager() { return this.packageManager; }

    public String generateScript() {
        try {
            String script = "module " + getName() + " " + getNodeClassName();
            String ps = generatePropertiesScript();
            if (ps.trim().length()!=0)
                script += " {\n" + ps + "}";
            script += "; ";

            if (this.getOutputs().size()>0 && (!this.getNodeClassName().equals("OutTerminal"))) {
                script += "connect " + getName() + " to ";
                for (ChainNode out : getOutputs())
                    script += out.getName() + ", ";
                script = script.substring(0, script.length()-2) + ";";
            }
            return script;
        } catch (IllegalArgumentException ie) {
            throw new IllegalArgumentException("While generating script of " + getPath() + ": ");
        }

    }

    private String generatePropertiesScript() {
        String script = "";
        boolean hasProps = false;
        for (PropertyInfo prop : getNodeInfo().getProperties()) {
            try {
                if (this.isPropertyGettable(prop.getName())) {
                    Object value = getProperty(prop.getName());
                    if (value!=null && !value.equals(prop.getDefaultValue()) &&
                            !((value instanceof List) && (((List)value).isEmpty()))) {
                        String pr;
                        try {
                            pr = InfoPrinter.printProperty(this, prop, value);
                        } catch (IllegalArgumentException ex) {
                            pr = prop.getName() + ": <unknown value>";
                        }
                        Tokenizer t = new Tokenizer(pr, ';');
                        while (t.hasMoreTokens()) {
                            script += "\t" + t.nextToken().trim() + ";\n";
                        }
                        hasProps = true;
                    }
                } else {
                    script += "\t## " + prop.getName() + " is not gettable. ;\n";
                }
            } catch (IllegalArgumentException ex) {
                ex.printStackTrace();
                throw new IllegalArgumentException("at property " + prop.getName() + " of module " + getName() + ": " + ex.getMessage());
            }
        }
        if (hasProps)
            return script;
        else return "";
    }

    @Override
    public boolean isPropertyGettable(String propName) {
        String methodName = "get" + props.getPropertyInfo(propName).getNameForMethod();
        Method[] methods = getClass().getMethods();
        for (Method method : methods)
            if (method.getName().equals(methodName))
                return true;
        return false;
    }

    @Override
    public boolean isPropertySettable(String propName) {
        String methodName = "set" + props.getPropertyInfo(propName).getNameForMethod();
        Method[] methods = getClass().getMethods();
        for (Method method : methods)
            if (method.getName().equals(methodName))
                return true;

        return false;
    }

    @Override
    public PropertyInfo getPropertyInfo(String prop) {
        return props.getPropertyInfo(prop);
    }

    @Override
    public Variable findVariable(String name) {
        Variable var = null;
        if (getParentChain()!=null) var = getParentChain().getVarTable().get(name);

        if (var==null && localVars!=null)
            var = localVars.get(name);

        return var;
    }

    private void invokeCommandMethod(Method cmdMethod, List<Variable> parameters) throws IllegalAccessException, IllegalArgumentException, InvocationTargetException {
            Object[] params = new Object[parameters.size()];

            int pIdx = 0;
            for (Variable v : parameters) {
                Class paramType = cmdMethod.getParameterTypes()[pIdx];
                Object value;
                if (v.getDataType().matches(DataType.EVENT)) value = v.getName();
                else value = v.getValue(this);

                if (paramType.isPrimitive()) {
                    if (paramType.equals(short.class))
                        params[pIdx] = ((Integer)DataType.INT.cast(value)).shortValue();
                    else if(paramType.equals(byte.class))
                        params[pIdx] = ((Integer)DataType.INT.cast(value)).byteValue();
                    else if (paramType.equals(int.class))
                        params[pIdx] = ((Integer)DataType.INT.cast(value)).intValue();
                    else if (paramType.equals(long.class))
                        params[pIdx] = ((Long)DataType.LONG.cast(value)).longValue();
                    else if (paramType.equals(float.class))
                        params[pIdx] = ((Double)DataType.FLOAT.cast(value)).floatValue();
                    else if (paramType.equals(double.class))
                        params[pIdx] = ((Double)DataType.FLOAT.cast(value)).doubleValue();
                    else if (paramType.equals(boolean.class))
                        params[pIdx] = ((Boolean)DataType.BOOLEAN.cast(value)).booleanValue();
                    else if (paramType.equals(char.class))
                        params[pIdx] = (char)((Integer)DataType.INT.cast(value)).intValue();
                } else {
                    DataType dest = DataType.findType(paramType);
                    if (dest==null)
                        params[pIdx] = paramType.cast(value);
                    else params[pIdx] = paramType.cast(dest.cast(value));
                }
                pIdx++;
            }

            cmdMethod.invoke(this, params);
    }
}
